package com.softwarecraftsmen.orogeny;

import com.softwarecraftsmen.orogeny.filing.*;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.*;

import static com.softwarecraftsmen.orogeny.filing.AbsoluteDirectoriesAndFiles.EmptyAbsoluteDirectoriesAndFiles;
import static com.softwarecraftsmen.orogeny.filing.AbsoluteDirectory.absoluteDirectory;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Locale.UK;
import static javax.xml.parsers.DocumentBuilderFactory.newInstance;

@SuppressWarnings("UnusedDeclaration")
public abstract class AbstractIntelliJConvenientBuildScript extends ConvenientBuildScript
{
    private final Map<String, AbsolutePath> simpleLibraries;
    private final Map<String, AbsolutePaths> complexLibraries;
    private final Map<String, String> intellijModulesWithMainClasses;

    protected AbstractIntelliJConvenientBuildScript()
    {
        simpleLibraries = new HashMap<>(10);
        complexLibraries = new HashMap<>(10);
        intellijModulesWithMainClasses = new HashMap<>(10);
    }

    protected final void intellijLibrary(final @NotNull String name, final @NotNull AbsolutePath absolutePath)
    {
        if (complexLibraries.containsKey(name))
        {
            throw new IllegalStateException(format(UK, "Multiple registration of a library %1$s not allowed", name));
        }
        simpleLibraries.put(name, absolutePath);
    }

    protected final void intellijLibrary(final @NotNull String name, final @NotNull AbsolutePaths absolutePaths)
    {
        if (simpleLibraries.containsKey(name))
        {
            throw new IllegalStateException(format(UK, "Multiple registration of a library %1$s not allowed", name));
        }
        complexLibraries.put(name, absolutePaths);
    }

	@NotNull
	protected final String intellijModuleHasMainClass(final @NotNull String moduleName, final @NotNull String packageName, final @NotNull String simpleMainClassName)
	{
		return intellijModuleHasMainClass(moduleName, packageName + '.' + simpleMainClassName);
	}

	@NotNull
    protected final String intellijModuleHasMainClass(final @NotNull String moduleName, final @NotNull String mainClassName)
    {
        if (intellijModulesWithMainClasses.containsKey(moduleName))
        {
            throw new IllegalStateException(format(UK, "Multiple registration of a module %1$s's main class (already %2$s, trying to register %3$s) not allowed", moduleName, intellijModulesWithMainClasses.get(moduleName), mainClassName));
        }
        intellijModulesWithMainClasses.put(moduleName, mainClassName);
		return mainClassName;
    }

    protected final void intellijProject(final @NotNull String projectName, final String dependsOn, final @NotNull String... groupsToExclude)
    {
        final Set<String> exclusions = new HashSet<>(asList(groupsToExclude));

        final AbsoluteFile file = source().file(projectName + ".ipr");

		final AbsoluteDirectory projectDir = file.parentDirectory();

		final File filePath = file.toFile();
        final String path = filePath.getPath();
        if (!file.exists())
        {
            throw new IllegalStateException(format(UK, "IntelliJ Project %1$s not found in intellijProject(\"%2$s\")", path, projectName));
        }

        final DocumentBuilderFactory documentBuilderFactory = newInstance();
        final DocumentBuilder documentBuilder;
        try
        {
            documentBuilder = documentBuilderFactory.newDocumentBuilder();
        }
        catch (ParserConfigurationException e)
        {
            throw new IllegalStateException(e);
        }

        final FileInputStream fileInputStream;
        try
        {
            fileInputStream = new FileInputStream(filePath);
        }
        catch (FileNotFoundException e)
        {
            throw new IllegalStateException(format(UK, "IntelliJ Module %1$s with IOException whilst opening found in intellijProject(\"%2$s\")", path, projectName), e);
        }

        final Document document;
        try
        {
            try
            {
                document = documentBuilder.parse(fileInputStream);
            }
            catch (SAXException e)
            {
                throw new IllegalStateException(format(UK, "IntelliJ Module %1$s with invalid XML found in intellijProject(\"%2$s\")", path, projectName), e);
            }
            catch (IOException e)
            {
                throw new IllegalStateException(format(UK, "IntelliJ Module %1$s with IOException whilst parsing found in intellijProject(\"%2$s\")", path, projectName), e);
            }
        }
        finally
        {
            try
            {
                fileInputStream.close();
            }
            catch (IOException ignored)
            {
            }
        }

        final Element documentElement = document.getDocumentElement();
        assertNodeNameIsElementNamed(documentElement, "project", path);
        assertAttributeMatches(documentElement, "version", "4", path);
        final NodeList components = documentElement.getElementsByTagName("component");
        final int length = components.getLength();
		Element projectModuleManager = null;
		Element libraryTable = null;
		for(int index = 0; index < length; index++)
		{
			final Element element = (Element) components.item(index);
			final String name = assertHasAttribute(element, "name", path);
			if (name.equals("ProjectModuleManager"))
			{
				if (projectModuleManager != null)
				{
					throw new IllegalStateException(format(UK, "IntelliJ Project %1$s with unknown XML (ProjectModuleManager) found in intellijProject(\"%2$s\")", path, projectName));
				}
				projectModuleManager = element;
			}
			if (name.equals("libraryTable"))
			{
				if (libraryTable != null)
				{
					throw new IllegalStateException(format(UK, "IntelliJ Project %1$s with unknown XML (libraryTable) found in intellijProject(\"%2$s\")", path, projectName));
				}
				libraryTable = element;
			}
		}

		registerIntelliJLibraries(path, libraryTable, projectDir);
		registerIntelliJModules(projectName, exclusions, path, projectModuleManager, dependsOn);
	}

	private void registerIntelliJLibraries(final String path, final Element libraryTable, final AbsoluteDirectory projectDir)
	{
		if (libraryTable == null)
		{
			return;
		}

		final String projectPath = projectDir.toFile().toString();

		final NodeList libraries = libraryTable.getElementsByTagName("library");
		final int libraryModules = libraries.getLength();
		for (int index = 0; index < libraryModules; index++)
		{
			final Element element = (Element) libraries.item(index);

			@NotNull final String libraryName = assertHasAttribute(element, "name", path);


			final NodeList classes = element.getElementsByTagName("CLASSES");
			final int classesLength = classes.getLength();
			for(int index2 = 0; index2 < classesLength; index2++)
			{
				final Element childNode = (Element) classes.item(index2);
				AbsolutePaths absolutePaths = EmptyAbsoluteDirectoriesAndFiles;
				final NodeList roots = childNode.getElementsByTagName("root");
				final int rootsLength = roots.getLength();
				for (int index3 = 0; index3 < rootsLength; index3++)
				{
					final Element root = (Element) roots.item(index3);
					@NotNull final String url = assertHasAttribute(root, "url", path);
					final String realUrl = url.replace("$PROJECT_DIR$", projectPath);
					final AbsolutePath absolutePath;
					if (realUrl.startsWith("jar://"))
					{
						final String withoutJar = realUrl.substring("jar://".length());
						final String realPath;
						if (withoutJar.endsWith("!/"))
						{
							realPath = withoutJar.substring(0, withoutJar.length() - 2);
						}
						else
						{
							realPath = withoutJar;
						}
						final File file = new File(realPath);
						final AbsoluteDirectory directory = absoluteDirectory(currentFileSystem(), file.getParentFile());
						absolutePath = directory.file(file.getName());
					}
					else if (realUrl.startsWith("file://"))
					{
						final File file = new File(realUrl.substring("file://".length()));
						absolutePath = absoluteDirectory(currentFileSystem(), file);
					}
					else
					{
						throw new IllegalStateException(realUrl);
					}
					absolutePaths = absolutePaths.and(absolutePath);
				}
				intellijLibrary(libraryName, absolutePaths);
			}
		}
	}

	private void registerIntelliJModules(final String projectName, final Set<String> exclusions, final String path, final Element projectModuleManager, final String... dependsOn)
	{
		if (projectModuleManager == null)
		{
			return;
		}

		final NodeList modules = ((Element) projectModuleManager.getElementsByTagName("modules").item(0)).getElementsByTagName("module");
		final int lengthModules = modules.getLength();
		for(int index = 0; index < lengthModules; index++)
		{
			final Element element = (Element) modules.item(index);

			// Empty if missing (ie a top-level group)
			@NotNull final String group = element.getAttribute("group");
			if (exclusions.contains(group))
			{
				continue;
			}

			final String filepath = assertHasAttribute(element, "filepath", path);
			if (!filepath.endsWith(".iml"))
			{
				continue;
			}

			if (!filepath.startsWith("$PROJECT_DIR$/"))
			{
				throw new IllegalStateException(format(UK, "IntelliJ Project %1$s with module filepath (%3$s) not starting $PROJECT_DIR$ in intellijProject(\"%2$s\")", path, projectName, filepath));
			}

			// Disgusting
			final String subPath = filepath.substring("$PROJECT_DIR$/".length());
			final String[] paths = subPath.split("\\/");
			final AbsoluteFile modulePath = source().subDirectory(paths).parent().file(paths[paths.length - 1]);

			final int lastIndex = filepath.lastIndexOf('/');
			final String moduleName;
			if (lastIndex == -1)
			{
				moduleName = filepath.substring(0, filepath.length() - 4);
			}
			else
			{
				final String substring = filepath.substring(lastIndex + 1);
				moduleName = substring.substring(0, substring.length() - 4);
			}
			intellijModule(moduleName, modulePath, dependsOn);
		}
	}

	protected final void intellijModule(final @NotNull String moduleName, @NotNull final AbsoluteFile moduleFile, final @NotNull String... dependsOn)
	{
        final File filePath = moduleFile.toFile();
        final String path = filePath.getPath();
        if (!moduleFile.exists())
        {
            throw new IllegalStateException(format(UK, "IntelliJ Module %1$s not found in intellijModule(\"%2$s\")", path, moduleName));
        }

        final AbsoluteDirectory source = source();
        final RelativeDirectory moduleFolderRoot = source.relativeDirectoryAssumingAbsoluteFileGivenIsSubdirectory(moduleFile);
        final String[] folders = moduleFolderRoot.toFolders();

        final DocumentBuilderFactory documentBuilderFactory = newInstance();
        final DocumentBuilder documentBuilder;
        try
        {
            documentBuilder = documentBuilderFactory.newDocumentBuilder();
        }
        catch (ParserConfigurationException e)
        {
            throw new IllegalStateException(e);
        }

        final FileInputStream fileInputStream;
        try
        {
            fileInputStream = new FileInputStream(filePath);
        }
        catch (FileNotFoundException e)
        {
            throw new IllegalStateException(format(UK, "IntelliJ Module %1$s with IOException whilst opening found in intellijModule(\"%2$s\")", path, moduleName), e);
        }

        final Document document;
        try
        {
            try
            {
                document = documentBuilder.parse(fileInputStream);
            }
            catch (SAXException e)
            {
                throw new IllegalStateException(format(UK, "IntelliJ Module %1$s with invalid XML found in intellijModule(\"%2$s\")", path, moduleName), e);
            }
            catch (IOException e)
            {
                throw new IllegalStateException(format(UK, "IntelliJ Module %1$s with IOException whilst parsing found in intellijModule(\"%2$s\")", path, moduleName), e);
            }
        }
        finally
        {
            try
            {
                fileInputStream.close();
            }
            catch (IOException ignored)
            {
            }
        }

        final Element documentElement = document.getDocumentElement();
        assertNodeNameIsElementNamed(documentElement, "module", path);
        assertAttributeMatches(documentElement, "type", "JAVA_MODULE", path);
        assertAttributeMatches(documentElement, "version", "4", path);

        AbsolutePaths dependentLibraries = new AbsoluteFiles(new LinkedHashSet<AbsoluteFile>());
        final List<String> dependentModules = new ArrayList<>(10);

        final NodeList components = documentElement.getElementsByTagName("component");
        final int componentsLength = components.getLength();
        for(int componentIndex = 0; componentIndex < componentsLength; componentIndex++)
        {
            final Element component = (Element) components.item(0);
            final String xmlModuleName = assertHasAttribute(component, "name", path);
            if (!"NewModuleRootManager".equals(xmlModuleName))
            {
                continue;
            }

            final NodeList contentChildNodes = component.getElementsByTagName("content");
            final int contentChildNodesLength = contentChildNodes.getLength();
            for(int index = 0; index < contentChildNodesLength; index++)
            {
                final Element contentChildNode = (Element) contentChildNodes.item(index);
                final NodeList sourceFolderChildNodes = contentChildNode.getElementsByTagName("sourceFolder");
                final int sourceFolderChildNodesLength = sourceFolderChildNodes.getLength();
                for(int sourceFolderIndex = 0; sourceFolderIndex < sourceFolderChildNodesLength; sourceFolderIndex++)
                {
                    final Element sourceFolderChildNode = (Element) contentChildNodes.item(sourceFolderIndex);

                    // we do not know what other values are
                    if (!"false".equals(sourceFolderChildNode.getAttribute("forTests")))
                    {
                        continue;
                    }

                    final String sourceFolderUrl = assertHasAttribute(sourceFolderChildNode, "sourceFolder", path);
                    final String startsWith = "file://$MODULE_DIR$";
                    if (!sourceFolderUrl.startsWith(startsWith))
                    {
                        throw new IllegalArgumentException("Do not understand sourceFolder urls that are not file://$MODULE_DIR$");
                    }

                    final String extraPath = sourceFolderUrl.substring(startsWith.length());

                    if (!extraPath.isEmpty())
                    {
                        throw new IllegalArgumentException("Only one source root, defined to be the module folder (not a src inside it) is allowed currently");
                    }
                }
            }

            final NodeList childNodes = component.getElementsByTagName("orderEntry");
            final int orderEntryLength = childNodes.getLength();
            for(int index = 0; index < orderEntryLength; index++)
            {
                final Element element = (Element) childNodes.item(index);
                final String type = assertHasAttribute(element, "type", path);
                if (type.equals("module"))
                {
                    dependentModules.add(assertHasAttribute(element, "module-name", path));
                }
                if (type.equals("library"))
                {
                    assertAttributeMatches(element, "level", "project", path);
                    final String name = assertHasAttribute(element, "name", path);
                    if (simpleLibraries.containsKey(name))
                    {
                        dependentLibraries = dependentLibraries.and(simpleLibraries.get(name));
                    }
                    else if (complexLibraries.containsKey(name))
                    {
                        dependentLibraries = dependentLibraries.and(complexLibraries.get(name));
                    }
                    else
                    {
                        throw new IllegalStateException(format(UK, "IntelliJ Module %1$s with unknown library %2$s found in intellijModule(\"%3$s\")", path, name, moduleName));
                    }
                }
            }
        }

        final String[] modulesNames = new String[dependentModules.size()];
        dependentModules.toArray(modulesNames);
		final CompileTask.DependsOnBeingCompiled.DependsOn.UsingClassPath usingClassPath = compileTask(moduleName, folders).dependsOnBeingCompiled(modulesNames).dependsOn(dependsOn).usingLibraries(dependentLibraries);
        if (intellijModulesWithMainClasses.containsKey(moduleName))
        {
            usingClassPath.withMainClass(intellijModulesWithMainClasses.get(moduleName));
        }
        else
        {
            usingClassPath.withoutMainClass();
        }
	}

    private void assertNodeNameIsElementNamed(Node node, String name, final String path)
    {
        if (!(node instanceof Element))
        {
            throw new IllegalStateException(format(UK, "IntelliJ File %1$s with unknown XML (node not element) found", path));
        }
        if (node.getNodeName().equals(name))
        {
            return;
        }
        throw new IllegalStateException(format(UK, "IntelliJ File %1$s with unknown XML (node name not %2$s was %3$s) found", path, name, node.getNodeName()));
    }

    private void assertAttributeMatches(Element element, String attributeName, String expectedValue, final String path)
    {
        final String attribute = assertHasAttribute(element, attributeName, path);
        if (!attribute.equals(expectedValue))
        {
            throw new IllegalStateException(format(UK, "IntelliJ File %1$s with unknown XML (attribute %2$s did not have expected value %3$s but %4$s) found", path, attributeName, expectedValue, attribute));
        }
    }

    private static String assertHasAttribute(Element element, String attributeName, String path)
    {
        if (!element.hasAttribute(attributeName))
        {
            throw new IllegalStateException(format(UK, "IntelliJ File %1$s with unknown XML (attribute %2$s was not present) found", path, attributeName));
        }
        return element.getAttribute(attributeName);
    }
}
